Retro Game w/ Modern UI Tools

Simon Mika

Simon Mika

Interim

  • CTO
  • Engineering Manager
  • Product Manager
  • Agile Advisor

Vice President of Operations, Europe

140 developers, testers, product & engineering managers & others

  • Perfect Doc Studio & Compass
  • Salesforce, Decisions & PEGA implementations
  • SaaS development

 meetup.com/uppsalajs 

 simonmika.com 

Retro Game w/ Modern UI Tools

github.com/simonmika/adventures-of-aron

State

State

  • hero
  • map
  • item

Hero


export class Hero {
	constructor(readonly position: Point, readonly facing: Direction = "down") {}
	move(direction: Direction): Hero {
		return new Hero(this.position.move(direction), direction)
	}
	face(direction: Direction): Hero {
		return new Hero(this.position, direction)
	}
}
						

Map


export class Map {
	readonly size: { readonly width: number; readonly height: number }
	private readonly tiles: readonly Tile[][]
	*layer(layer: Tile.Layer): Generator {
		for (const tile of this.tiles.flat())
			if (tile.layer == layer)
				yield tile
	}
	*items(): Generator {
		for (const tile of this.tiles.flat())
			if (tile.item)
				yield tile.item
	}
}
						

Tile


export abstract class Tile {
	abstract readonly type: Type
	abstract readonly layer: Layer
	abstract walkable: boolean
	constructor(public readonly position: Point, public readonly map: Map, public readonly item?: Item) {}
	place(item: Item): Tile {
		return new types[this.type]!(this.position, this.map, item)
	}
	collect(): Tile {
		return new types[this.type]!(this.position, this.map) as Tile
	}
}
						

Item


export class Item {
	constructor(readonly type: Item.Type, readonly position: Point) {}
}
						

Game


export class Game {
	constructor(readonly map: Map, readonly hero: Hero) {}

	move(direction: Direction): Game {
		let hero = this.hero.move(direction)
		const tile = this.map.get(hero.position)
		if (!tile.walkable)
			hero = this.hero.face(direction)
		const map = tile.walkable && tile.item ? this.map.set(tile.collect()) : this.map
		return new Game(map, hero)
	}
	static async fetch(url: string): Promise {
		const level = await (await fetch(url)).json()
		return Level.is(level) ? Level.load(level) : undefined
	}
}
						

User Interface

Tiles

Z-order

Game


@Component({ tag: "aron-game", styleUrl: "style.css", scoped: true, })
export class AronGame implements ComponentWillLoad {
	@State() game?: model.Game
	async componentWillLoad(): Promise {
		this.game = await model.Game.fetch("/assets/levels/level0.json")
	}
	render() {
		return <Host>	{!this.game ? "loading.." : (
					<Fragment>
						<aron-layer layer="ground" map={this.game.map}></aron-layer>
						<aron-items map={this.game.map}></aron-items>
						<aron-hero hero={this.game.hero}></aron-hero>
						<aron-layer layer="canopy" map={this.game.map}></aron-layer>
					</Fragment>
				)}</Host> }
}
						

Interactivity


@Listen("keydown", { target: "window" })
	onKeyDown(event: KeyboardEvent) {
		let direction: model.Direction | undefined
		switch (event.key) {
			case "ArrowLeft": direction = "left";	break
			case "ArrowRight": direction = "right";	break
			case "ArrowUp": direction = "up";	break
			case "ArrowDown": direction = "down";	break
		}
		if (direction)
			this.game = this.game?.move(direction)
	}
						

Layer


@Component({ tag: "aron-layer", styleUrl: "style.css", scoped: true, })
export class AronLayer {
	@Prop() layer: model.Tile.Layer
	@Prop() map: model.Map
	render() {
		return (
			<Host>
				{[...this.map.layer(this.layer)].map(tile => (
					<aron-tile tile={tile}></aron-tile>
				))}
			</Host>
		)
	}
}
						

Tile


@Component({ tag: "aron-tile",	styleUrl: "style.css",	scoped: true, })
export class AronTile {
	@Prop() tile: model.Tile
	render() {
		return <Host
				style={{
					left: (this.tile.position.x * 64).toString() + "px",
					top: (this.tile.position.y * 64).toString() + "px",
				}}>
				<img src={`/assets/tiles/${this.tile.type}.svg`} />
			</Host>
	}
}
						

Hero


@Component({ tag: "aron-hero",	styleUrl: "style.css",	scoped: true,})
export class AronHero {
	@Prop() hero: model.Hero
	render() {
		return (
			<Host
				style={{
					left: (this.hero.position.x * 64).toString() + "px",
					top: (this.hero.position.y * 64).toString() + "px",
				}}>
				<img src={`/assets/hero/${this.hero.facing}.svg`} />
			</Host>
		)
	}
}
						

Hero CSS


:host {
	display: block;
	position: absolute;
}
:host {
	transition: left 0.5s ease-in-out, top 0.5s ease-in-out;
}
						

Conclusions

  • state vs rendering
  • absolute positioning

Questions

Simon Mika · simon.mika@cogneco.se · 0707-680 380